﻿// <copyright file="BestSellers.cs" company="Microsoft Corporation">
// Copyright (c) 2008 All Right Reserved
// </copyright>
// <author>Michael S. Scherotter</author>
// <email>mischero@microsoft.com</email>
// <date>2009-03-03</date>
// <summary>New York Times Best Sellers API for Silverlight</summary>

namespace NYTimes
{
    using System;
    using System.Collections.Generic;
    using System.Collections.ObjectModel;
    using System.ComponentModel;
    using System.Globalization;
    using System.Linq;
    using System.Net;
    using System.Text;
    using System.Windows;
    using System.Xml.Linq;
    using NYTimes.Core;
    using NYTimes.Data;
    using Synergist;

    /// <summary>
    /// New York Times Best Sellers (Books) API
    /// </summary>
    /// <remarks>http://developer.nytimes.com/docs/best_sellers_api
    /// <list type="table">
    /// <listheader>
    /// <term>Application Resource Key</term>
    /// <description>Key to add to application resources</description>
    /// </listheader>
    /// <item>
    /// <term>
    /// NYTBestSellersKey
    /// </term>
    /// <description>Bestsellers API Key</description>
    /// </item>
    /// <item>
    /// <term>
    /// BestsellersQPS
    /// </term>
    /// <description>Rate Limiting (default 8 QPS)</description>
    /// </item>
    /// </list>
    /// </remarks>
    public class Bestsellers : INotifyPropertyChanged
    {
        #region Fields
        /// <summary>
        /// the Application resource key for the Best Sellers API
        /// </summary>
        private const string NYTBestSellersKey = "NYTBestSellersKey";

        /// <summary>
        /// the isolated storage site settings key for the best sellers list
        /// </summary>
        private const string NYTBestSellersLists = "NYTBestSellersLists";

        /// <summary>
        /// 8 QPS rate limiting
        /// </summary>
        private static RateLimiting limiting = new RateLimiting("NYTBestsellers", 8);

        /// <summary>
        /// the list name
        /// </summary>
        private string listName;

        /// <summary>
        /// the API key
        /// </summary>
        private string apiKey;

        /// <summary>
        /// is the list loading?
        /// </summary>
        private bool isLoading;

        /// <summary>
        /// list offset
        /// </summary>
        private int offset;

        /// <summary>
        /// list names
        /// </summary>
        private IEnumerable<string> listNames;

        /// <summary>
        /// List web client
        /// </summary>
        private WebClient client;

        /// <summary>
        /// the web client for getting the list of lists.
        /// </summary>
        private WebClient listClient;
        #endregion

        #region Constructors
        /// <summary>
        /// Initializes a new instance of the Bestsellers class.
        /// </summary>
        public Bestsellers()
        {
            this.List = new ObservableCollection<Book>();

            this.Version = "v2";

            if (Application.Current.Resources.Contains(NYTBestSellersKey))
            {
                this.ApiKey = Application.Current.Resources[NYTBestSellersKey] as string;
            }

            if (SampleData.IsInDesignTool)
            {
                var xml = Core.SampleData.GetSampleData("BookLists.xml");

                if (!string.IsNullOrEmpty(xml))
                {
                    this.ParseListNames(xml);
                }

                xml = Core.SampleData.GetSampleData("HardcoverFicton.xml");

                if (!string.IsNullOrEmpty(xml))
                {
                    this.ParseList(xml);
                }
            }
        }
        #endregion

        #region INotifyPropertyChanged Members

        /// <summary>
        /// Property changed event
        /// </summary>
        public event PropertyChangedEventHandler PropertyChanged;

        #endregion

        #region Properties
        /// <summary>
        /// Gets a list of best sellers
        /// </summary>
        public ObservableCollection<Book> List { get; private set; }

        /// <summary>
        /// Gets a value indicating whether the list is loading
        /// </summary>
        public bool IsLoading
        {
            get
            {
                return this.isLoading;
            }

            private set
            {
                if (this.isLoading != value)
                {
                    this.isLoading = value;

                    if (this.PropertyChanged != null)
                    {
                        this.PropertyChanged(this, new PropertyChangedEventArgs("IsLoading"));
                    }
                }
            }
        }

        /// <summary>
        /// Gets the API version
        /// </summary>
        [DefaultValue("v2")]
        public string Version { get; private set; }

        /// <summary>
        /// Gets or sets the API key
        /// </summary>
        [Description("The NYT Bestsellers API Key")]
        public string ApiKey
        {
            get
            {
                return this.apiKey;
            }

            set
            {
                this.apiKey = value;

                if (this.PropertyChanged != null)
                {
                    this.PropertyChanged(this, new PropertyChangedEventArgs("ApiKey"));
                }
            }
        }

        /// <summary>
        /// Gets or sets the publication date of the list
        /// </summary>
        [Description("The Publication date of the list"),
        TypeConverter(typeof(DateTimeTypeConverter))]
        public DateTime PublicationDate { get; set; }

        /// <summary>
        /// Gets the offset
        /// </summary>
        public int Offset 
        {
            get
            {
                return this.offset;
            }

            private set
            {
                this.offset = value;

                if (value == 0)
                {
                    this.List.Clear();
                }

                this.GetList();
            }
        }

        /// <summary>
        /// Gets the Best Sellers List Names
        /// </summary>
        /// <remarks>http://developer.nytimes.com/docs/best_sellers_api#h3-list-names</remarks> 
        public IEnumerable<string> ListNames
        {
            get
            {
                if (this.listNames == null)
                {
                    this.GetListNames();
                }

                return this.listNames;
            }

            private set
            {
                if (this.listNames != value)
                {
                    this.listNames = value;

                    if (this.PropertyChanged != null)
                    {
                        this.PropertyChanged(this, new PropertyChangedEventArgs("ListNames"));
                    }
                }
            }
        }

        /// <summary>
        /// Gets or sets the number of results to get
        /// </summary>
        [Description("the number of results to get.")]
        public int ResultsToGet { get; set; }

        /// <summary>
        /// Gets or sets the current List Name to
        /// </summary>
        [Description("The name of the list query. Get the list names with the ListNames collection.")]
        public string ListName
        {
            get
            {
                return this.listName;
            }

            set
            {
                if (this.listName != value)
                {
                    this.listName = value;

                    if (this.PropertyChanged != null)
                    {
                        this.PropertyChanged(this, new PropertyChangedEventArgs("ListName"));
                    }

                    this.Offset = 0;
                }
            }
        }
        #endregion

        #region Implementation
        /// <summary>
        /// Convert a string value in an element to a Uri or null
        /// </summary>
        /// <param name="element">the parent element</param>
        /// <param name="name">the name of the child element with a URI value</param>
        /// <returns>a URI or null</returns>
        private static Uri UriValue(XElement element, string name)
        {
            var child = element.Element(name).Value;

            if (string.IsNullOrEmpty(child))
            {
                return null;
            }

            child = child.Trim();

            if (string.IsNullOrEmpty(child))
            {
                return null;
            }

            return new Uri(child, UriKind.Absolute);
        }

        /// <summary>
        /// Get the list names
        /// </summary>
        private void GetListNames()
        {
            if (SampleData.IsInDesignTool)
            {
                return;
            }

            if (string.IsNullOrEmpty(this.ApiKey))
            {
                this.ListNames = new string[] { "Empty ApiKey for BestSellers API" };

                if (this.PropertyChanged != null)
                {
                    this.PropertyChanged(this, new PropertyChangedEventArgs("ListNames"));
                }

                return;
            }

            if (this.listClient != null)
            {
                return;
            }

            this.listClient = new WebClient();

            var uriString = string.Format(CultureInfo.InvariantCulture, "http://api.nytimes.com/svc/books/{0}/lists/names.xml?api-key={1}", this.Version, this.ApiKey);

            var address = new Uri(uriString, UriKind.Absolute);

            System.Diagnostics.Debug.WriteLine(address);

            this.listClient.DownloadStringCompleted += new DownloadStringCompletedEventHandler(this.OnListNamesReturned);

            limiting.MakeWebRequest(this.listClient, address);
        }

        /// <summary>
        /// Web client list names returned
        /// </summary>
        /// <param name="sender">the web client</param>
        /// <param name="e">the download string completed event arguments</param>
        private void OnListNamesReturned(object sender, DownloadStringCompletedEventArgs e)
        {
            if (e.Error != null)
            {
                System.Diagnostics.Debug.WriteLine(e.Error.Message);

                this.listClient = null;

                return;
            }

            this.ParseListNames(e.Result);

            this.listClient = null;
        }

        /// <summary>
        /// Parse the list names
        /// </summary>
        /// <param name="xml">the XML of the list names</param>
        private void ParseListNames(string xml)
        {
            var doc = XDocument.Parse(xml);

            this.ListNames = from listName in doc.Descendants("list_name")
                             select listName.Value;

            if (this.PropertyChanged != null)
            {
                this.PropertyChanged(this, new PropertyChangedEventArgs("ListNames"));
            }
        }

        /// <summary>
        /// Gets a bestseller list
        /// </summary>
        private void GetList()
        {
            if (string.IsNullOrEmpty(this.ListName))
            {
                return;
            }

            if (SampleData.IsInDesignTool)
            {
                return;
            }

            this.IsLoading = true;

            if (this.client != null)
            {
                this.client.CancelAsync();

                this.client = null;
            }

            this.client = new WebClient();

            string uriString = string.Format(CultureInfo.InvariantCulture, "http://api.nytimes.com/svc/books/{0}/lists/", this.Version);

            var builder = new StringBuilder(uriString);

            if (this.PublicationDate != DateTime.MinValue)
            {
                builder.AppendFormat(CultureInfo.InvariantCulture, "{0:yyyy-MM-dd}/", this.PublicationDate);
            }

            builder.AppendFormat(CultureInfo.InvariantCulture, "{0}.xml?api-key={1}", this.ListName.Replace(' ', '-'), this.ApiKey);

            if (this.Offset > 0)
            {
                builder.AppendFormat(CultureInfo.InvariantCulture, "&offset={0}", this.Offset);
            }

            var address = new Uri(builder.ToString(), UriKind.Absolute);

            System.Diagnostics.Debug.WriteLine(address);

            this.client.DownloadStringCompleted += new DownloadStringCompletedEventHandler(this.OnListReturned);

            limiting.MakeWebRequest(this.client, address);
        }

        /// <summary>
        /// web client list returned event handler
        /// </summary>
        /// <param name="sender">the web client</param>
        /// <param name="e">the download string completed event arguments</param>
        private void OnListReturned(object sender, DownloadStringCompletedEventArgs e)
        {
            if (e.Error != null)
            {
                System.Diagnostics.Debug.WriteLine(e.Error.Message);

                if (e.Cancelled)
                {
                    this.IsLoading = false;
                }

                return;
            }

            this.client = null;

            var xml = e.Result;

            var numResults = this.ParseList(xml);

            if (this.List.Count() < numResults && this.List.Count() < this.ResultsToGet)
            {
                this.Offset += 20;
            }
            else
            {
                this.IsLoading = false;
            }
        }

        /// <summary>
        /// Parse the list of results
        /// </summary>
        /// <param name="xml">the XML for the result set</param>
        /// <returns>the total number of results</returns>
        private int ParseList(string xml)
        {
            var doc = XDocument.Parse(xml);

            var resultSet = doc.Element("result_set");

            var list = from book in resultSet.Descendants("book")
                       let bookDetails = book.Element("book_details")
                       where bookDetails != null
                       let bookDetail = bookDetails.Element("book_detail")
                       let agegroup = bookDetail.Element("age_group")
                       let contributorNote = bookDetail.Element("contributor_note")
                       select new Book()
                       {
                           Rank = new BookRank()
                           {
                               Rank = int.Parse(book.Element("rank").Value, System.Globalization.CultureInfo.InvariantCulture),
                               ListName = book.Element("list_name").Value,
                               BestsellersDate = DateTime.Parse(book.Element("bestsellers_date").Value, CultureInfo.InvariantCulture),
                               PublishedDate = DateTime.Parse(book.Element("published_date").Value, CultureInfo.InvariantCulture),
                               WeeksOnList = int.Parse(book.Element("weeks_on_list").Value, System.Globalization.CultureInfo.InvariantCulture),
                               RankLastWeek = int.Parse(book.Element("rank_last_week").Value, System.Globalization.CultureInfo.InvariantCulture),
                               Asterisk = int.Parse(book.Element("asterisk").Value, System.Globalization.CultureInfo.InvariantCulture) == 1,
                               Dagger = int.Parse(book.Element("dagger").Value, System.Globalization.CultureInfo.InvariantCulture) == 1,
                           },
                           Details = bookDetails == null ? null : new BookDetail()
                           {
                               AgeGroup = bookDetail.Element("age_group").Value,
                               Author = bookDetail.Element("author").Value,
                               Contributor = bookDetail.Element("contributor").Value,
                               ContributorNote = contributorNote == null ? null : contributorNote.Value,
                               Description = bookDetail.Element("description").Value,
                               Price = double.Parse(bookDetail.Element("price").Value, System.Globalization.CultureInfo.InvariantCulture),
                               Publisher = bookDetail.Element("publisher").Value,
                               Title = bookDetail.Element("title").Value,
                           },

                           ISBNs = book.Element("isbns") == null ? null : from isbn in book.Element("isbns").Elements("isbn")
                                                                          select new ISBN()
                                                                          {
                                                                              ISBN10 = isbn.Element("isbn10").Value,
                                                                              ISBN13 = isbn.Element("isbn13").Value
                                                                          },
                           Reviews = book.Element("reviews") == null ? null : from review in book.Element("reviews").Elements("review")
                                                                              select new Review()
                                                                              {
                                                                                  BookReview = UriValue(review, "book_review_link"),
                                                                                  ArticleChapter = UriValue(review, "article_chapter_link"),
                                                                                  FirstChapter = UriValue(review, "first_chapter_link"),
                                                                                  SundayReview = UriValue(review, "sunday_review_link"),
                                                                              }
                       };

            foreach (var book in list)
            {
                if (this.List.Count >= this.ResultsToGet)
                {
                    break;
                }

                this.List.Add(book);
            }

            var numResults = int.Parse(resultSet.Element("num_results").Value, CultureInfo.InvariantCulture);

            return numResults;
        }
        #endregion
    }
}
